// This Pine Script® code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © AlgoAlpha

//@version=6

// Inputs
indicator("Stochastic Z-Score [AlgoAlpha]", "AlgoAlpha - Stoch Z-Score")

src = input.source(hlc3, title = "Source", group = "Calculation", tooltip = "Price series used for all calculations. Changing it alters the base values from which the Z-score and momentum are derived.")
len = input.int(21, title = "Length", group = "Calculation", tooltip = "Look-back period for moving averages and deviations. Higher values give smoother, slower signals; lower values make the oscillator faster and more sensitive.")
hist = input.bool(false, title = "Show Histogram", group = "Appearance", tooltip = "Toggle display of the raw, unsmoothed Z-score as columns. Useful for visualising noise layer beneath the smoothed oscillator.")
green = input.color(#00ffbb, title = "Bullish Colour", group = "Appearance", tooltip = "Primary colour for bullish visual elements. Adjust for preferred palette – affects bars, fills, and labels when momentum is positive.")
red   = input.color(#ff1100, title = "Bearish Colour", group = "Appearance", tooltip = "Primary colour for bearish visual elements. Adjust for preferred palette – affects bars, fills, and labels when momentum is negative.")

basis = ta.sma(src, len)
zscore = (src - basis) / ta.stdev(src, len)
stochZ = ta.stoch(zscore, zscore, zscore, len)
scaledSZ= stochZ/25-2
smoothed_scaled = ta.hma(scaledSZ, len)
ltm = ta.alma(zscore, len, 0, 0.1)

// Plots & Visuals
zscore_color = color.new(color.from_gradient(zscore, -2, 2, red, green), math.max(math.min(int((1-math.abs(zscore)/4)*100), 70), 70))
plot(zscore, color = zscore_color, linewidth = 2, display = hist ? display.all : display.none, style = plot.style_columns, title = "Z-Score")

smoothed_scaled_prev = smoothed_scaled[1]
smoothed_scaled_color = color.new(color.gray, smoothed_scaled > smoothed_scaled_prev ? 0 : 50)
main  = plot(smoothed_scaled,  title = "Smoothed Oscillator", color = smoothed_scaled_color, linewidth = 3)
main1 = plot(smoothed_scaled_prev, title = "Oscillator (Lag 1)", color = color.new(color.gray, smoothed_scaled > smoothed_scaled_prev ? 20 : 70), display = display.none)

ltm_color = color.new(ltm > 0 ? green : red, 70)
longtm = plot(ltm, title = "Long-Term Momentum (ALMA)", color = ltm_color, display = display.none)

mid = plot(0, title = "Zero Line", display = display.none)
upper_color = color.new(color.from_gradient(math.max(0, smoothed_scaled), 0, 2, color.gray, red), 70)
lower_color = color.new(color.from_gradient(math.min(0, smoothed_scaled), -2, 0, green, color.gray), 70)
u = plot(2, "Upper", upper_color, linewidth = 3)
d = plot(-2, "Down", lower_color, linewidth = 3)
u1 = plot(2.5, "Upper Outer", color.from_gradient(math.max(0, smoothed_scaled), 0, 2.5, color.gray, red), linewidth = 3)
d1 = plot(-2.5, "Down Outer", color.from_gradient(math.min(0, smoothed_scaled), -2.5, 0, green, color.gray), linewidth = 3)

plotchar(ta.cross(smoothed_scaled, smoothed_scaled_prev) ? smoothed_scaled_prev : na, "Momentum Shift", "●", location.absolute, chart.fg_color, size = size.tiny)
plotchar(ta.crossover(smoothed_scaled, smoothed_scaled_prev) and ltm > 0 and smoothed_scaled_prev < -2 ? -3 : na, "Bullish Reversal", "▲", location.absolute, green, size = size.tiny)
plotchar(ta.crossunder(smoothed_scaled, smoothed_scaled_prev) and ltm < 0 and smoothed_scaled_prev > 2 ? 3 : na, "Bearish Reversal", "▼", location.absolute, red, size = size.tiny)

plotshape(ta.crossover(smoothed_scaled, smoothed_scaled_prev) and ltm > 0 and smoothed_scaled_prev < -2, "Bullish Trend", shape.labelup, location.belowbar, color.new(green, 50), text = "▲", textcolor = chart.fg_color, size = size.small, force_overlay = true)
plotshape(ta.crossunder(smoothed_scaled, smoothed_scaled_prev) and ltm < 0 and smoothed_scaled_prev > 2, "Bearish Trend", shape.labeldown, location.abovebar, color.new(red, 50), text = "▼", textcolor = chart.fg_color, size = size.small, force_overlay = true)

upper_fill_color = color.new(color.from_gradient(math.max(0, smoothed_scaled), 0, 2, chart.bg_color, red), 50)
upper_fill_color_100 = color.new(color.from_gradient(math.max(0, smoothed_scaled), 0, 2, chart.bg_color, red), 100)
lower_fill_color = color.new(color.from_gradient(math.min(0, smoothed_scaled), -2, 0, green, chart.bg_color), 100)
lower_fill_color_50 = color.new(color.from_gradient(math.min(0, smoothed_scaled), -2, 0, green, chart.bg_color), 50)
fill(u1, u, 2.5, 2, upper_fill_color, upper_fill_color_100, "Upper Fill")
fill(d, d1, -2, -2.5, lower_fill_color, lower_fill_color_50, "Lower Fill")
fill(main, main1, color.new(color.gray, smoothed_scaled > smoothed_scaled_prev ? 20 : 70), "Oscillator Fill")
fill(longtm, mid, ltm, 0, ltm_color, color.new(ltm > 0 ? green : red, 90))

// Alerts
alertcondition(ta.cross(smoothed_scaled, smoothed_scaled_prev), title = "Momentum Shift", message = "Stochastic Z-Score momentum direction changed.")
alertcondition(ta.crossover(smoothed_scaled, smoothed_scaled_prev) and ltm > 0 and smoothed_scaled_prev < -2, title = "Bullish Reversal", message = "Bullish reversal detected (oscillator crosses up while long-term momentum is positive).")
alertcondition(ta.crossunder(smoothed_scaled, smoothed_scaled_prev) and ltm < 0 and smoothed_scaled_prev > 2, title = "Bearish Reversal", message = "Bearish reversal detected (oscillator crosses down while long-term momentum is negative).")
alertcondition(ta.crossover(smoothed_scaled, 0), title = "Oscillator Crosses Above Zero", message = "Oscillator crossed above the zero line (bullish momentum).")
alertcondition(ta.crossunder(smoothed_scaled, 0), title = "Oscillator Crosses Below Zero", message = "Oscillator crossed below the zero line (bearish momentum).")
alertcondition(ta.crossover(ltm, 0), title = "Long-Term Momentum Bullish", message = "Long-term momentum (ALMA) crossed above zero.")
alertcondition(ta.crossunder(ltm, 0), title = "Long-Term Momentum Bearish", message = "Long-term momentum (ALMA) crossed below zero.")
alertcondition(smoothed_scaled > 2.5, title = "Overbought", message = "Oscillator entered overbought zone (> 2.5).")
alertcondition(smoothed_scaled < -2.5, title = "Oversold", message = "Oscillator entered oversold zone (< -2.5).")